/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 * 
 * The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Original Code is pythonext code.
 * 
 * The Initial Developer of the Original Code is Todd Whiteman.
 * Portions created by the Initial Developer are Copyright (C) 2000-2008
 * the Initial Developer. All Rights Reserved.
 * 
 * Contributor(s):
 *   Todd Whiteman <twhitema@gmail.com> (original author)
 * 
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 * 
 * ***** END LICENSE BLOCK ***** */

Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");

// The SFTP Protocol XPCOM class.
function SFTPProtocol() { }

SFTPProtocol.prototype = {
    classDescription: "SFTPProtocol XPCOM Component",
    classID:          Components.ID("{48178ff7-ce90-43bf-be56-8c170880fb25}"),
    contractID:       "@mozilla.org/network/protocol;1?name=sftp",
    QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIProtocolHandler]),

    // Example URI:  "sftp://myhost/my/path/file.txt"

    scheme: "sftp",
    defaultPort: 22,
    protocolFlags: Components.interfaces.nsIProtocolHandler.URI_LOADABLE_BY_ANYONE,

    newURI: function(aSpec, aOriginCharset, aBaseURI) {
        var url = Components.classes["@mozilla.org/network/standard-url;1"].
                 createInstance(Components.interfaces.nsIStandardURL);
        url.init(Components.interfaces.nsIStandardURL.URLTYPE_AUTHORITY,
                 this.defaultPort, aSpec, aOriginCharset, aBaseURI);
        return url.QueryInterface(Components.interfaces.nsIURI);
    },
    
    newChannel: function(aURI) {
        var channel = new SSHChannel();
        channel.URI = aURI;
        channel.originalURI = aURI;
        return channel;
    },

    allowPort: function(port, scheme) {
        return true;
    }
};


// The SSH Protocol XPCOM class.
function SSHProtocol() { }

SSHProtocol.prototype = {
    classDescription: "SSHProtocol XPCOM Component",
    classID:          Components.ID("{84274cc8-abd3-4eef-8155-5d1cc6924992}"),
    contractID:       "@mozilla.org/network/protocol;1?name=ssh",
    QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIProtocolHandler]),

    // Example URI:  "ssh://me@myhost/"

    scheme: "ssh",
    defaultPort: 22,
    protocolFlags: Components.interfaces.nsIProtocolHandler.URI_LOADABLE_BY_ANYONE,

    newURI: function(aSpec, aOriginCharset, aBaseURI) {
        var url = Components.classes["@mozilla.org/network/standard-url;1"].
                 createInstance(Components.interfaces.nsIStandardURL);
        url.init(Components.interfaces.nsIStandardURL.URLTYPE_AUTHORITY,
                 this.defaultPort, aSpec, aOriginCharset, aBaseURI);
        return url.QueryInterface(Components.interfaces.nsIURI);
    },
    
    newChannel: function(aURI) {
        var channel = new SSHChannel();
        channel.URI = aURI;
        channel.originalURI = aURI;
        return channel;
    },

    allowPort: function(port, scheme) {
        return true;
    }
};


// The SSH channel XPCOM class.
function SSHChannel() {
    this._ssh = Components.classes['@pythonext.mozdev.org/sshConnection;1'].
                 createInstance(Components.interfaces.pyISSHConnection);
    this._isPending = true;
}

SSHChannel.prototype = {
    classDescription: "SSHChannel XPCOM Component",
    classID:          Components.ID("{ed90fa0a-5213-4a74-9431-1c42f960d283}"),
    contractID:       "@pythonext.mozdev.org/SSHChannel;1",
    QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIRequest,
                                           Components.interfaces.nsIChannel,
                                           Components.interfaces.nsIUploadChannel,
                                           Components.interfaces.nsIResumableChannel,
                                           Components.interfaces.pyISSHCommand,
                                           Components.interfaces.pyISSHInteraction]),

    /* internal */

    _async_type: null,
    _command: null,
    _termType: null,
    _convertStream: null,
    _ssh: null,
    _authSvc: null,
    listener: null,
    context: null,

    logException: function(message, ex) {
        var consoleSvc = Components.classes["@mozilla.org/consoleservice;1"].
                           getService(Components.interfaces.nsIConsoleService);
        consoleSvc.logStringMessage(message + ": " + ex);
        Components.utils.reportError(ex);
    },

    /* nsIRequest */

    name: "SSH Channel",
    loadGroup: null,
    loadFlags: 0,
    status: 0,

    isPending: function() {
        if (this._ssh) {
            return this._ssh.isPending;
        }
        return this._isPending;
    },

    cancel: function(status) {
        if (this._ssh) {
            this._ssh.cancel(status);
            this.status = this._ssh.status;
        } else {
            self.status = status;
        }
    },

    suspend: function() {
        if (this._ssh) {
            return this._ssh.suspend();
        }
        throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
    },

    resume: function() {
        if (this._ssh) {
            return this._ssh.resume();
        }
        throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
    },

    /* nsIUploadChannel */

    uploadStream: null,

    setUploadStream: function(aStream, aContentType, aContentLength) {
        this.uploadStream = aStream;
        this.contentType = aContentType;
        this.contentLength = aContentLength;
    },

    /* nsIResumableChannel */

    _bytesDownloaded: 0,
    _bytesToDownload: 0,
    _resumeAtPos: 0,
    _isResumedDownload: false,
    entityID: null,

    resumeAt: function(aStartPos, aEntityId) {
        this._ssh.resumeAt(aStartPos, aEntityId);
        this._bytesDownloaded = 0;
        this._bytesToDownload = 0;
        this._resumeAtPos = aStartPos;
        this._isResumedDownload = true;
    },

    /* nsIChannel */

    originalURI: "",
    URI: "",
    contentLength: -1,
    contentType: "",
    contentCharset: "",
    owner: null,
    notificationCallbacks: null,
    progressCallback: null,
    securityInfo: null,

    open: function() {
        throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
    },

    asyncOpen: function(aListener, aContext) {
        try {
            this._async_type = "asyncOpen";
            this.listener = aListener;
            this.context = aContext;
            this._startChannel();
        } catch (ex) {
            this.logException("SSH asyncOpen exception", ex);
        }
    },

    /* pyISSHCommand and pyISSHInteraction */

    asyncRunCommand: function(aListener, aCommand, aContext) {
        try {
            this._async_type = "asyncRunCommand";
            this.listener = aListener;
            this._command = aCommand;
            this.context = aContext;
            this._startChannel();
        } catch (ex) {
            this.logException("SSH asyncRunCommand exception", ex);
        }
    },

    asyncInteract: function(aListener, aTerminalType, aContext) {
        try {
            this._async_type = "asyncInteract";
            this.listener = aListener;
            this._termType = aTerminalType;
            this.context = aContext;
            this._startChannel();
        } catch (ex) {
            this.logException("SSH asyncInteract exception", ex);
        }
    },

    sendStdin: function(aData) {
        try {
            this._ssh.sendStdin(aData);
        } catch (ex) {
            this.logException("SSH sendStdin exception", ex);
        }
    },

    /* channel internal implementation */

    _getAuthSvc: function() {
        if (this._authSvc) {
            // We've already got the auth service before.
            return this._authSvc;
        }

        // Try getting the auth service from the notificationCallbacks
        if (this.notificationCallbacks) {
            try {
                this._authSvc = this.notificationCallbacks.QueryInterface(Components.interfaces.nsIAuthPrompt2);
            } catch(ex) {
                // Not available
            }
        }

        // Try getting the auth service from the loadgroup
        if (!this._authSvc && this.loadGroup) {
            try {
                this._authSvc = this.loadGroup.QueryInterface(Components.interfaces.nsIAuthPrompt2);
            } catch(ex) {
                // Not available
            }
        }

        // Fallback to the standard auth prompt service.
        if (!this._authSvc) {
            //authSvc = Components.classes["@mozilla.org/network/default-auth-prompt;1"].\
            //            getService(Components.interfaces.nsIAuthPrompt)
            ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"].
                        getService(Components.interfaces.nsIPromptFactory);
            this._authSvc = ww.getPrompt(null, Components.interfaces.nsIAuthPrompt2);
        }

        return this._authSvc;
    },

    _startChannel: function() {
        try {
            this._ssh.setURI(this.URI);

            if (this.notificationCallbacks) {
                try {
                    this.progressCallback = this.notificationCallbacks.QueryInterface(Components.interfaces.nsIProgressEventSink);
                    this.progressCallback.onStatus(this, this.context, Components.interfaces.nsISocketTransport.STATUS_RESOLVING, "Resolving");
                } catch (ex) {
                    // Not supported.
                    this.progressCallback = null;
                }
            }
            if (this.loadGroup) {
                this.loadGroup.addRequest(this, null);
            }
            if (this.uploadStream) {
                this._ssh.setUploadStream(this.uploadStream, this.contentType, this.contentLength);
            }

            var listener_wrapper = (function(self, sftp_channel) {
                // pyISSHStreamListener instance.
                return {
                    // Callback function to enable the sftp python channel to
                    // prompt for the username/password.
                    promptAuth: function(level, authInfo) {
                        try {
                            //return self._getAuthSvc().promptAuth(self, level, authInfo);
                            authSvc = self._getAuthSvc();
                            return authSvc.promptAuth(self, level, authInfo);
                        } catch (ex) {
                            self.logException("SSH promptAuth Exception", ex);
                        }
                        return false;
                    },
                    onStartRequest: function(context) {
                        try {
                            self.status = sftp_channel.status;
                            // Set entity id, so it can be resumed.
                            self.entityID = sftp_channel.entityID;
                            if (self.status == Components.results.NS_OK) {
                                if (self.progressCallback) {
                                    self.progressCallback.onStatus(self, self.context, Components.interfaces.nsISocketTransport.STATUS_SENDING_TO, "Sending");
                                }
                                self.contentType = sftp_channel.contentType;
                                self.contentCharset = sftp_channel.contentCharset;
                                if (!sftp_channel.isDirectoryListing) {
                                    //dump("fileSize is " + sftp_channel.fileSize + "\n");
                                    self.contentLength = sftp_channel.fileSize;
                                    if (self._isResumedDownload) {
                                        self._bytesToDownload = self.contentLength - self._resumeAtPos;
                                    }
                                }
                            }
                            self.listener.onStartRequest (self, self.context);
                            if (self.status == Components.results.NS_OK) {
                                if (self.progressCallback) {
                                    self.progressCallback.onStatus(self, self.context, Components.interfaces.nsISocketTransport.STATUS_RECEIVING_FROM, "Receiving");
                                }
                            }
                        } catch (ex) {
                            self.logException("SSH onStartRequest Exception", ex);
                        }
                    },
                    onDataAvailable: function(channel, context, stream, offset, length) {
                        try {
                            var bytes_available = stream.available();
                            self.listener.onDataAvailable (self, self.context, stream, 0, bytes_available);
                            // We need to report different progress mechanics
                            // to the download manager for resumed downloads.
                            if (self._isResumedDownload && self.progressCallback) {
                                self._bytesDownloaded += bytes_available;
                                self.progressCallback.onProgress(self, self.context, self._bytesDownloaded, self._bytesToDownload);
                            }
                        } catch (ex) {
                            self.logException("SSH onDataAvailable Exception", ex);
                        }
                    },
                    onStopRequest: function(context, status) {
                        try {
                            self._isPending = false;
                            self.status = sftp_channel.status;
                            self.listener.onStopRequest (self, self.context, self.status);
                            if (self.loadGroup) {
                                try {
                                    self.loadGroup.removeRequest (self, null, self.status);
                                } catch (ex) {
                                    // Already removed from the load group?
                                }
                            }
                        } catch (ex) {
                            self.logException("SSH onStopRequest Exception: ", ex);
                        }
                    }
                };
            })(this, this._ssh);

            if (this.progressCallback) {
                this.progressCallback.onStatus(this, this.context, Components.interfaces.nsISocketTransport.STATUS_CONNECTING_TO, "Connecting");
            }


            if (this._async_type == "asyncOpen") {

                this._ssh.asyncOpen(listener_wrapper, this.context);

            } else if (this._async_type == "asyncRunCommand") {

                var ssh = this._ssh.QueryInterface(Components.interfaces.pyISSHCommand);
                ssh.asyncRunCommand(listener_wrapper, this._command, this.context);

            } else if (this._async_type == "asyncInteract") {

                var ssh_interact = this._ssh.QueryInterface(Components.interfaces.pyISSHInteraction);
                ssh_interact.asyncInteract(listener_wrapper, this._termType, this.context);

            } else {
                throw "Unknown async operation: " + this._async_type;
            }

        } catch (ex) {
            this.logException("SSH _startChannel Exception", ex);
        }
    }
};



// XPCOM registration.
var components = [SFTPProtocol, SSHProtocol, SSHChannel];
if (XPCOMUtils.generateNSGetFactory)
    var NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
else
    var NSGetModule = XPCOMUtils.generateNSGetModule(components);
